package com.riiablo.mpq_bytebuf.util;

import io.netty.buffer.ByteBuf;

import com.riiablo.logger.LogManager;
import com.riiablo.logger.Logger;

public final class Exploder {
  private Exploder() {}

  private static final Logger log = LogManager.getLogger(Exploder.class);

  static final int CTYPE_BINARY = 0;
  static final int CTYPE_ASCII = 1;

  static final int RET_NO_ERROR = 0;
  static final int RET_INVALID_DICTSIZE = 1;
  static final int RET_INVALID_MODE = 2;
  static final int RET_BAD_DATA = 3;
  static final int RET_ABORT = 4;

  static final int DCL_OK = 0;
  static final int DCL_STREAM_END = 1;
  static final int DCL_NEED_DICT = 2;
  static final int DCL_CONTINUE = 10;
  static final int DCL_GET_INPUT = 11;

  static int[] GenCodeTable(int[] codes, int[] bits) {
    final int rollOver = 0x100;
    final int[] gen = new int[rollOver];
    for (int i = 0, s = bits.length; i < s; i++) {
      final int len = 1 << bits[i];
      for (int j = codes[i]; j < rollOver; j += len) {
        gen[j] = i;
      }
    }
    return gen;
  }

  static int[] GenAsciiTable(int[] codes, int[] bits) {
    // throw new UnsupportedOperationException();
    return null;
  }

  static final int[/*0x40*/] DistBits = {
      0x02, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06,
      0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
      0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
      0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08,
  };

  static final int[/*0x40*/] DistCode = {
      0x03, 0x0D, 0x05, 0x19, 0x09, 0x11, 0x01, 0x3E, 0x1E, 0x2E, 0x0E, 0x36, 0x16, 0x26, 0x06, 0x3A,
      0x1A, 0x2A, 0x0A, 0x32, 0x12, 0x22, 0x42, 0x02, 0x7C, 0x3C, 0x5C, 0x1C, 0x6C, 0x2C, 0x4C, 0x0C,
      0x74, 0x34, 0x54, 0x14, 0x64, 0x24, 0x44, 0x04, 0x78, 0x38, 0x58, 0x18, 0x68, 0x28, 0x48, 0x08,
      0xF0, 0x70, 0xB0, 0x30, 0xD0, 0x50, 0x90, 0x10, 0xE0, 0x60, 0xA0, 0x20, 0xC0, 0x40, 0x80, 0x00,
  };

  static final int[/*0x100*/] GenDistCode = GenCodeTable(DistCode, DistBits);

  static final int[/*0x10*/] ExLenBits = {
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
  };

  static final int[/*0x10*/] LenBase = {
      0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
      0x0008, 0x000A, 0x000E, 0x0016, 0x0026, 0x0046, 0x0086, 0x0106,
  };

  static final int[/*0x10*/] LenBits = {
      0x03, 0x02, 0x03, 0x03, 0x04, 0x04, 0x04, 0x05,
      0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x07, 0x07,
  };

  static final int[/*0x10*/] LenCode = {
      0x05, 0x03, 0x01, 0x06, 0x0A, 0x02, 0x0C, 0x14,
      0x04, 0x18, 0x08, 0x30, 0x10, 0x20, 0x40, 0x00,
  };

  static final int[/*0x100*/] GenLenCode = GenCodeTable(LenCode, LenBits);

  static final int[/*0x100*/] ChBits = {
      0x0B, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x07, 0x0C, 0x0C, 0x07, 0x0C, 0x0C,
      0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0D, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
      0x04, 0x0A, 0x08, 0x0C, 0x0A, 0x0C, 0x0A, 0x08, 0x07, 0x07, 0x08, 0x09, 0x07, 0x06, 0x07, 0x08,
      0x07, 0x06, 0x07, 0x07, 0x07, 0x07, 0x08, 0x07, 0x07, 0x08, 0x08, 0x0C, 0x0B, 0x07, 0x09, 0x0B,
      0x0C, 0x06, 0x07, 0x06, 0x06, 0x05, 0x07, 0x08, 0x08, 0x06, 0x0B, 0x09, 0x06, 0x07, 0x06, 0x06,
      0x07, 0x0B, 0x06, 0x06, 0x06, 0x07, 0x09, 0x08, 0x09, 0x09, 0x0B, 0x08, 0x0B, 0x09, 0x0C, 0x08,
      0x0C, 0x05, 0x06, 0x06, 0x06, 0x05, 0x06, 0x06, 0x06, 0x05, 0x0B, 0x07, 0x05, 0x06, 0x05, 0x05,
      0x06, 0x0A, 0x05, 0x05, 0x05, 0x05, 0x08, 0x07, 0x08, 0x08, 0x0A, 0x0B, 0x0B, 0x0C, 0x0C, 0x0C,
      0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D,
      0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D,
      0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D,
      0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
      0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
      0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C,
      0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D,
      0x0D, 0x0D, 0x0C, 0x0C, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D,
  };

  static final int[/*0x100*/] ChCode = {
      0x0490, 0x0FE0, 0x07E0, 0x0BE0, 0x03E0, 0x0DE0, 0x05E0, 0x09E0,
      0x01E0, 0x00B8, 0x0062, 0x0EE0, 0x06E0, 0x0022, 0x0AE0, 0x02E0,
      0x0CE0, 0x04E0, 0x08E0, 0x00E0, 0x0F60, 0x0760, 0x0B60, 0x0360,
      0x0D60, 0x0560, 0x1240, 0x0960, 0x0160, 0x0E60, 0x0660, 0x0A60,
      0x000F, 0x0250, 0x0038, 0x0260, 0x0050, 0x0C60, 0x0390, 0x00D8,
      0x0042, 0x0002, 0x0058, 0x01B0, 0x007C, 0x0029, 0x003C, 0x0098,
      0x005C, 0x0009, 0x001C, 0x006C, 0x002C, 0x004C, 0x0018, 0x000C,
      0x0074, 0x00E8, 0x0068, 0x0460, 0x0090, 0x0034, 0x00B0, 0x0710,
      0x0860, 0x0031, 0x0054, 0x0011, 0x0021, 0x0017, 0x0014, 0x00A8,
      0x0028, 0x0001, 0x0310, 0x0130, 0x003E, 0x0064, 0x001E, 0x002E,
      0x0024, 0x0510, 0x000E, 0x0036, 0x0016, 0x0044, 0x0030, 0x00C8,
      0x01D0, 0x00D0, 0x0110, 0x0048, 0x0610, 0x0150, 0x0060, 0x0088,
      0x0FA0, 0x0007, 0x0026, 0x0006, 0x003A, 0x001B, 0x001A, 0x002A,
      0x000A, 0x000B, 0x0210, 0x0004, 0x0013, 0x0032, 0x0003, 0x001D,
      0x0012, 0x0190, 0x000D, 0x0015, 0x0005, 0x0019, 0x0008, 0x0078,
      0x00F0, 0x0070, 0x0290, 0x0410, 0x0010, 0x07A0, 0x0BA0, 0x03A0,
      0x0240, 0x1C40, 0x0C40, 0x1440, 0x0440, 0x1840, 0x0840, 0x1040,
      0x0040, 0x1F80, 0x0F80, 0x1780, 0x0780, 0x1B80, 0x0B80, 0x1380,
      0x0380, 0x1D80, 0x0D80, 0x1580, 0x0580, 0x1980, 0x0980, 0x1180,
      0x0180, 0x1E80, 0x0E80, 0x1680, 0x0680, 0x1A80, 0x0A80, 0x1280,
      0x0280, 0x1C80, 0x0C80, 0x1480, 0x0480, 0x1880, 0x0880, 0x1080,
      0x0080, 0x1F00, 0x0F00, 0x1700, 0x0700, 0x1B00, 0x0B00, 0x1300,
      0x0DA0, 0x05A0, 0x09A0, 0x01A0, 0x0EA0, 0x06A0, 0x0AA0, 0x02A0,
      0x0CA0, 0x04A0, 0x08A0, 0x00A0, 0x0F20, 0x0720, 0x0B20, 0x0320,
      0x0D20, 0x0520, 0x0920, 0x0120, 0x0E20, 0x0620, 0x0A20, 0x0220,
      0x0C20, 0x0420, 0x0820, 0x0020, 0x0FC0, 0x07C0, 0x0BC0, 0x03C0,
      0x0DC0, 0x05C0, 0x09C0, 0x01C0, 0x0EC0, 0x06C0, 0x0AC0, 0x02C0,
      0x0CC0, 0x04C0, 0x08C0, 0x00C0, 0x0F40, 0x0740, 0x0B40, 0x0340,
      0x0300, 0x0D40, 0x1D00, 0x0D00, 0x1500, 0x0540, 0x0500, 0x1900,
      0x0900, 0x0940, 0x1100, 0x0100, 0x1E00, 0x0E00, 0x0140, 0x1600,
      0x0600, 0x1A00, 0x0E40, 0x0640, 0x0A40, 0x0A00, 0x1200, 0x0200,
      0x1C00, 0x0C00, 0x1400, 0x0400, 0x1800, 0x0800, 0x1000, 0x0000,
  };

  static final int[/*0x100*/] GenChCode = GenAsciiTable(ChCode, ChBits);

  static final int[] BIT_MASKS = new int[Integer.SIZE + 1];
  static {
    for (int i = 1; i <= Integer.SIZE; i++) {
      BIT_MASKS[i] = (BIT_MASKS[i - 1] << 1) + 1;
    }
  }

  static final class Tuple {
    final byte[] in;
    int pIn;
    int pInLimit;

    final byte[] out;
    int pOut;
    int pOutLimit;

    final int cType;
    final int dictSize;
    final int dictSizeMask;

    int bitBuffer;
    int extraBits;

    Tuple(
        final byte[] in,
        final int pIn,
        final int pInLimit,
        final byte[] out,
        final int pOut,
        final int pOutLimit,
        final int cType,
        final int dictSize,
        final int dictSizeMask,
        final int bitBuffer,
        final int extraBits
    ) {
      this.in = in;
      this.pIn = pIn;
      this.pInLimit = pInLimit;
      this.out = out;
      this.pOut = pOut;
      this.pOutLimit = pOutLimit;
      this.cType = cType;
      this.dictSize = dictSize;
      this.dictSizeMask = dictSizeMask;
      this.bitBuffer = bitBuffer;
      this.extraBits = extraBits;
    }
  }

  public static int explode(final ByteBuf in, final ByteBuf out) {
    final int exitCode = explode(
        in.array(), in.arrayOffset() + in.readerIndex(), in.readableBytes(),
        out.array(), out.arrayOffset() + out.writerIndex(), out.writableBytes());
    out.writerIndex(out.capacity());
    return exitCode;
  }

  public static int explode(
      final byte[] in, int COffs, int CSize,
      final byte[] out, int FOffs, int FSize
  ) {
    int pIn = COffs;
    int pInLimit = pIn + CSize;
    if ((pInLimit - pIn) <= 4) return RET_BAD_DATA;

    int pOut = FOffs;
    int pOutLimit = pOut + FSize;

    final int cType = in[pIn++] & 0xff;
    if (cType != CTYPE_ASCII && cType != CTYPE_BINARY) return RET_INVALID_MODE;

    final int dictSize = in[pIn++] & 0xff;
    if (dictSize < 4 || 6 < dictSize) return RET_INVALID_DICTSIZE;

    final int dictSizeMask = 0xffff >> (0x10 - dictSize);

    final int bitBuffer = in[pIn++] & 0xff;
    final int extraBits = 0;
    Tuple t = new Tuple(
        in,
        pIn,
        pInLimit,
        out,
        pOut,
        pOutLimit,
        cType,
        dictSize,
        dictSizeMask,
        bitBuffer,
        extraBits
    );

    return expand(t) != 0x306 ? RET_NO_ERROR : RET_ABORT;
  }

  static int expand(Tuple t) {
    int b, result;
    while ((result = b = decodeLiteral(t)) < 0x305) {
      if (b >= 0x100) {
        int repeatLen = b - 0xfe;
        final int copyBack;
        if ((copyBack = decodeDist(t, repeatLen)) == 0) {
          result = 0x306;
          break;
        }

        int target = t.pOut;
        int source = target - copyBack;
        final byte[] out = t.out;
        t.pOut += repeatLen;
        while (repeatLen-- > 0) {
          out[target++] = out[source++];
        }
      } else {
        t.out[t.pOut++] = (byte) b;
      }
    }

    return result;
  }

  static boolean skip(Tuple t, int bits) {
    if (bits <= t.extraBits) {
      t.bitBuffer >>>= bits;
      t.extraBits -= bits;
      return false;
    }

    t.bitBuffer >>>= t.extraBits;
    if (t.pIn >= t.pInLimit) {
      t.pIn = t.pInLimit;
      return true;
    }

    t.bitBuffer |=  ((t.in[t.pIn++] & 0xff) << Byte.SIZE);
    t.bitBuffer >>>= (bits - t.extraBits);
    t.extraBits = (t.extraBits - bits) + Byte.SIZE;
    return false;
  }

  static int decodeLiteral(Tuple t) {
    int lenCode;
    if ((t.bitBuffer & 1) == 1) {
      if (skip(t, 1)) return 0x306;

      lenCode = GenLenCode[t.bitBuffer & 0xff];
      if (skip(t, LenBits[lenCode])) return 0x306;

      int exLenBits;
      if ((exLenBits = ExLenBits[lenCode]) != 0) {
        int exLen = t.bitBuffer & BIT_MASKS[exLenBits]; // & ((1 << exLenBits) - 1)
        if (skip(t, exLenBits) && (lenCode + exLen) != 0x10e) return 0x306;
        lenCode = LenBase[lenCode] + exLen;
      }

      return lenCode + 0x100;
    }

    if (skip(t, 1)) return 0x306;
    if (t.cType == CTYPE_BINARY) {
      final int b = t.bitBuffer & 0xff;
      if (skip(t, Byte.SIZE)) return 0x306;
      return b;
    }

    // ascii
    throw new UnsupportedOperationException();
  }

  static int decodeDist(Tuple t, int repeatLen) {
    final int distCode = GenDistCode[t.bitBuffer & 0xff];
    final int distBits = DistBits[distCode];
    if (skip(t, distBits)) return 0;

    final int distance;
    if (repeatLen == 2) {
      distance = (distCode << 2) | (t.bitBuffer & 0x3);
      if (skip(t, 2)) return 0;
    } else {
      distance = (distCode << t.dictSize) | (t.bitBuffer & t.dictSizeMask);
      if (skip(t, t.dictSize)) return 0;
    }

    return distance + 1;
  }
}
